// FileSearchEngine: Async directory-based search and open per doc design
// API: searchInDirs(directories, keyword, options?) => { success, matches:[], best }
// Options: { maxResults=50, depthLimit=6, timeoutMs=8000, earlyStopOnExact=true }

const fs = require('fs')
const fsp = fs.promises
const path = require('path')

function defaultDirs() {
  const home = require('os').homedir()
  return [
    path.join(home, 'Desktop'),
    path.join(home, 'Documents'),
    path.join(home, 'Downloads'),
  ]
}

function isHidden(name) {
  return name.startsWith('.')
}

function preprocess(s) {
  if (!s || typeof s !== 'string') return ''
  return s.trim().toLowerCase()
}

function baseName(name) {
  const ext = path.extname(name)
  return ext ? name.slice(0, -ext.length) : name
}

function scoreName(name, key) {
  const n = name.toLowerCase()
  if (!key) return 0
  if (n === key) return 100
  if (baseName(n) === baseName(key)) return 95
  if (n.startsWith(key)) return 80
  if (n.includes(key)) return 60
  return 0
}

class FileSearchEngine {
  constructor() {
    this.cache = new Map() // key -> { ts, ttlMs, result }
    this.currentToken = 0
  }

  cancelAll() {
    this.currentToken++
  }

  _cacheKey(dirs, key, options) {
    return JSON.stringify({ d: dirs.map(d=>d.toLowerCase()), k: key.toLowerCase(), o: { mr: options.maxResults, dl: options.depthLimit } })
  }

  _getCache(dirs, key, options) {
    const k = this._cacheKey(dirs, key, options)
    const item = this.cache.get(k)
    if (!item) return null
    if (Date.now() - item.ts > (item.ttlMs || 30000)) {
      this.cache.delete(k)
      return null
    }
    return item.result
  }

  _setCache(dirs, key, options, result, ttlMs = 30000) {
    const k = this._cacheKey(dirs, key, options)
    this.cache.set(k, { ts: Date.now(), ttlMs, result })
  }

  async searchInDirs(directories, keyword, options = {}) {
    const dirs = Array.isArray(directories) && directories.length ? directories : defaultDirs()
    const key = preprocess(keyword)
    const opt = Object.assign({ maxResults: 50, depthLimit: 6, timeoutMs: 8000, earlyStopOnExact: true }, options)
    const cached = this._getCache(dirs, key, opt)
    if (cached) return { success: true, matches: cached, best: cached[0] || null, fromCache: true }

    const token = ++this.currentToken
    const deadline = Date.now() + opt.timeoutMs
    const matches = []

    async function walkDir(dir, depth) {
      if (Date.now() > deadline) return
      if (depth > opt.depthLimit) return
      try {
        const entries = await fsp.readdir(dir, { withFileTypes: true })
        for (const entry of entries) {
          if (token !== this.currentToken) return // cancelled
          if (matches.length >= opt.maxResults) return
          const full = path.join(dir, entry.name)
          if (entry.isDirectory()) {
            if (!isHidden(entry.name)) {
              await walkDir.call(this, full, depth + 1)
            }
          } else {
            const lower = entry.name.toLowerCase()
            if (isHidden(entry.name) || lower.endsWith('.tmp') || lower.startsWith('~$')) continue
            const s = scoreName(entry.name, key)
            if (s > 0) {
              matches.push({ id: full, name: entry.name, path: full, type: 'file', score: s })
              if (opt.earlyStopOnExact && (lower === key || baseName(lower) === baseName(key))) {
                return // early stop on exact
              }
            }
          }
        }
      } catch (_) { /* ignore inaccessible dirs */ }
    }

    for (const d of dirs) {
      if (token !== this.currentToken) break
      await walkDir.call(this, d, 0)
      if (opt.earlyStopOnExact && matches.length && (matches[0].name.toLowerCase() === key || baseName(matches[0].name.toLowerCase()) === baseName(key))) {
        break
      }
    }

    matches.sort((a, b) => {
      const exactA = a.name.toLowerCase() === key || baseName(a.name.toLowerCase()) === baseName(key)
      const exactB = b.name.toLowerCase() === key || baseName(b.name.toLowerCase()) === baseName(key)
      if (exactA && !exactB) return -1
      if (!exactA && exactB) return 1
      if (b.score !== a.score) return b.score - a.score
      return a.name.localeCompare(b.name)
    })

    this._setCache(dirs, key, opt, matches)
    return { success: matches.length > 0, matches, best: matches[0] || null }
  }
}

module.exports = { FileSearchEngine, defaultDirs }
